package com.ihr360.job.core.entity;

import com.ihr360.job.core.BatchStatus;
import com.ihr360.job.core.ExitStatus;
import com.ihr360.job.core.JobParameters;
import com.ihr360.job.core.item.ExecutionContext;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * Batch domain object representing the execution of a job.
 */
public class JobExecution extends Entity {
    public static final String JOB_OWNER_ID = "OWNER_ID";
    public static final String JOB_OWNER_NAME = "OWNER_NAME";
    public static final String OWNER_JOB_ID = "OWNER_JOB_ID";

    private final JobParameters jobParameters;

    private JobInstance jobInstance;

    private volatile Collection<StepExecution> stepExecutions = new CopyOnWriteArraySet<StepExecution>();

    private volatile BatchStatus status = BatchStatus.STARTING;

    private volatile Date startTime = null;

    private volatile Date createTime = new Date(System.currentTimeMillis());

    private volatile Date endTime = null;

    private volatile Date lastUpdated = null;

    private volatile ExitStatus exitStatus = ExitStatus.UNKNOWN;

    private volatile ExecutionContext executionContext = new ExecutionContext();

    private volatile int stepCount;

    private volatile String ownerId;
    private volatile String ownerName;
    private volatile String ownerJobId;
    private volatile String jobName;

    private transient volatile List<Throwable> failureExceptions = new CopyOnWriteArrayList<Throwable>();

    private final String jobConfigurationName;

    public JobExecution(JobExecution original) {
        this.jobParameters = original.getJobParameters();
        this.jobInstance = original.getJobInstance();
        this.stepExecutions = original.getStepExecutions();
        this.status = original.getStatus();
        this.startTime = original.getStartTime();
        this.createTime = original.getCreateTime();
        this.endTime = original.getEndTime();
        this.lastUpdated = original.getLastUpdated();
        this.exitStatus = original.getExitStatus();
        this.stepCount = original.getStepCount();
        this.executionContext = original.getExecutionContext();
        this.failureExceptions = original.getFailureExceptions();
        this.jobConfigurationName = original.getJobConfigurationName();
        this.ownerId = original.getOwnerId();
        this.ownerName = original.getOwnerName();
        this.ownerJobId = original.getOwnerJobId();
        this.setId(original.getId());
        this.setVersion(original.getVersion());
    }

    /**
     * Because a JobExecution isn't valid unless the job is set, this
     * constructor is the only valid one from a modeling point of view.
     *
     * @param job the job of which this execution is a part
     */
    public JobExecution(JobInstance job, Long id, JobParameters jobParameters, String jobConfigurationName, int stepCount) {
        super(id);
        this.jobInstance = job;
        this.jobParameters = jobParameters == null ? new JobParameters() : jobParameters;
        this.jobConfigurationName = jobConfigurationName;
        this.stepCount = stepCount;
    }

    public JobExecution(JobInstance job, JobParameters jobParameters, String jobConfigurationName, int stepCount) {
        this(job, null, jobParameters, jobConfigurationName, stepCount);
    }

    public JobExecution(Long id, JobParameters jobParameters, String jobConfigurationName, int stepCount) {
        this(null, id, jobParameters, jobConfigurationName, stepCount);
    }

    /**
     * Constructor for transient (unsaved) instances.
     *
     * @param job the enclosing {@link JobInstance}
     */
    public JobExecution(JobInstance job, JobParameters jobParameters) {
        this(job, null, jobParameters, null, 0);
    }

    public JobExecution(Long id, JobParameters jobParameters) {
        this(null, id, jobParameters, null, 0);
    }

    public JobExecution(Long id) {
        this(null, id, null, null, 0);
    }

    public JobParameters getJobParameters() {
        return this.jobParameters;
    }

    public Date getEndTime() {
        return endTime;
    }

    public void setJobInstance(JobInstance jobInstance) {
        this.jobInstance = jobInstance;
    }

    public void setEndTime(Date endTime) {
        this.endTime = endTime;
    }

    public Date getStartTime() {
        return startTime;
    }

    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }

    public BatchStatus getStatus() {
        return status;
    }

    /**
     * Set the value of the status field.
     *
     * @param status the status to set
     */
    public void setStatus(BatchStatus status) {
        this.status = status;
    }

    /**
     * Upgrade the status field if the provided value is greater than the
     * existing one. Clients using this method to set the status can be sure
     * that they don't overwrite a failed status with an successful one.
     *
     * @param status the new status value
     */
    public void upgradeStatus(BatchStatus status) {
        this.status = this.status.upgradeTo(status);
    }

    /**
     * Convenience getter for for the id of the enclosing job. Useful for DAO
     * implementations.
     *
     * @return the id of the enclosing job
     */
    public Long getJobId() {
        if (jobInstance != null) {
            return jobInstance.getId();
        }
        return null;
    }

    /**
     * @param exitStatus
     */
    public void setExitStatus(ExitStatus exitStatus) {
        this.exitStatus = exitStatus;
    }

    /**
     * @return the exitCode
     */
    public ExitStatus getExitStatus() {
        return exitStatus;
    }

    /**
     * @return the Job that is executing.
     */
    public JobInstance getJobInstance() {
        return jobInstance;
    }

    public int getStepCount() {
        return stepCount;
    }

    public void setStepCount(int stepCount) {
        this.stepCount = stepCount;
    }

    public String getOwnerId() {
        return ownerId;
    }

    public void setOwnerId(String ownerId) {
        this.ownerId = ownerId;
    }

    public String getOwnerJobId() {
        return ownerJobId;
    }

    public void setOwnerJobId(String ownerJobId) {
        this.ownerJobId = ownerJobId;
    }

    public String getOwnerName() {
        return ownerName;
    }

    public void setOwnerName(String ownerName) {
        this.ownerName = ownerName;
    }

    public String getJobName() {
        return jobName;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    /**
     * Accessor for the step executions.
     *
     * @return the step executions that were registered
     */
    public Collection<StepExecution> getStepExecutions() {
        return Collections.unmodifiableList(new ArrayList<StepExecution>(stepExecutions));
    }

    /**
     * Register a step execution with the current job execution.
     *
     * @param stepName the name of the step the new execution is associated with
     */
    public StepExecution createStepExecution(String stepName) {
        StepExecution stepExecution = new StepExecution(stepName, this);
        this.stepExecutions.add(stepExecution);
        return stepExecution;
    }

    /**
     * Test if this {@link JobExecution} indicates that it is running. It should
     * be noted that this does not necessarily mean that it has been persisted
     * as such yet.
     *
     * @return true if the end time is null
     */
    public boolean isRunning() {
        return endTime == null;
    }

    /**
     * Test if this {@link JobExecution} indicates that it has been signalled to
     * stop.
     *
     * @return true if the status is {@link BatchStatus#STOPPING}
     */
    public boolean isStopping() {
        return status == BatchStatus.STOPPING;
    }

    /**
     * Signal the {@link JobExecution} to stop. Iterates through the associated
     * {@link StepExecution}s, calling {@link StepExecution#setTerminateOnly()}.
     */
    public void stop() {
        for (StepExecution stepExecution : stepExecutions) {
            stepExecution.setTerminateOnly();
        }
        status = BatchStatus.STOPPING;
    }

    /**
     * Sets the {@link ExecutionContext} for this execution
     *
     * @param executionContext the context
     */
    public void setExecutionContext(ExecutionContext executionContext) {
        this.executionContext = executionContext;
    }

    /**
     * Returns the {@link ExecutionContext} for this execution. The content is
     * expected to be persisted after each step completion (successful or not).
     *
     * @return the context
     */
    public ExecutionContext getExecutionContext() {
        return executionContext;
    }

    /**
     * @return the time when this execution was created.
     */
    public Date getCreateTime() {
        return createTime;
    }

    /**
     * @param createTime creation time of this execution.
     */
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public String getJobConfigurationName() {
        return this.jobConfigurationName;
    }

    /**
     * Package private method for re-constituting the step executions from
     * existing instances.
     *
     * @param stepExecution
     */
    void addStepExecution(StepExecution stepExecution) {
        stepExecutions.add(stepExecution);
    }

    /**
     * Get the date representing the last time this JobExecution was updated in
     * the JobRepository.
     *
     * @return Date representing the last time this JobExecution was updated.
     */
    public Date getLastUpdated() {
        return lastUpdated;
    }

    /**
     * Set the last time this JobExecution was updated.
     *
     * @param lastUpdated
     */
    public void setLastUpdated(Date lastUpdated) {
        this.lastUpdated = lastUpdated;
    }

    public List<Throwable> getFailureExceptions() {
        return failureExceptions;
    }

    /**
     * Add the provided throwable to the failure exception list.
     *
     * @param t
     */
    public synchronized void addFailureException(Throwable t) {
        this.failureExceptions.add(t);
    }

    /**
     * Return all failure causing exceptions for this JobExecution, including
     * step executions.
     *
     * @return List&lt;Throwable&gt; containing all exceptions causing failure for
     * this JobExecution.
     */
    public synchronized List<Throwable> getAllFailureExceptions() {

        Set<Throwable> allExceptions = new HashSet<Throwable>(failureExceptions);
        for (StepExecution stepExecution : stepExecutions) {
            allExceptions.addAll(stepExecution.getFailureExceptions());
        }

        return new ArrayList<Throwable>(allExceptions);
    }

    /**
     * Deserialize and ensure transient fields are re-instantiated when read
     * back
     */
    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        failureExceptions = new ArrayList<Throwable>();
    }

    /*
     * (non-Javadoc)
     *
     * @see org.springframework.batch.core.domain.Entity#toString()
     */
    @Override
    public String toString() {
        return super.toString()
                + String.format(", startTime=%s, endTime=%s, lastUpdated=%s, status=%s, exitStatus=%s, job=[%s], jobParameters=[%s]",
                startTime, endTime, lastUpdated, status, exitStatus, jobInstance, jobParameters);
    }

    /**
     * Add some step executions.  For internal use only.
     *
     * @param stepExecutions step executions to add to the current list
     */
    public void addStepExecutions(List<StepExecution> stepExecutions) {
        if (stepExecutions != null) {
            this.stepExecutions.removeAll(stepExecutions);
            this.stepExecutions.addAll(stepExecutions);
        }
    }
}